package yawning.mew.bear;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;

import yawning.mew.ProcEffect;
import yawning.mew.Procs;
import yawning.mew.BuffsDebuffs.Debuff;
import yawning.mew.ProcEffect.Effect;
import yawning.mew.bear.BearThreatSimulation.Action;
import yawning.mew.bear.BearThreatSimulation.SimulatorProperty;
import yawning.mew.character.CharacterModel.AttackResult;
import yawning.mew.sim.SimulationAction;
import yawning.mew.sim.SimulationEngine;
import yawning.mew.sim.SimulationLog;
import yawning.mew.sim.SimulationProcStatus;
import yawning.mew.sim.SimulationEventTargetMagic.TargetMagicCallback;
import yawning.mew.sim.SimulationEventTargetSwing.TargetSwingCallback;
import yawning.mew.sim.SimulationEventTargetYellow.TargetYellowCallback;
import yawning.mew.sim.SimulationProc;
import yawning.mew.sim.SimulationManager.Property;
import yawning.mew.sim.SimulationProc.EffectCallback;
import yawning.mew.sim.SimulationTarget;
import yawning.mew.sim.Strategy.IStatus;

public class BearThreatSimulator implements IStatus {
	SimulationEngine mSimEngine;
	BearModel mModel;

	boolean mIsLogging = false;
	SimulationLog mLog;

	SimulationTarget mActiveTarget;
	BearTargetData mActiveTargetData;
	List<SimulationTarget> mTargetList;

	boolean mUnlimitedRage = false;
	boolean mShouldFF;

	boolean mHavePotion;
	boolean mPrePotion;

	double mRage;	// Apparently fractional rage is preserved.
	long mRageSpent;
	long mPrimalFuryRageGained;
	long mPrimalMadnessRageGained;
	long mNaturalReactionRageGained;
	long mRageGainedCountThreat;
	long mRageGainedCountThreatApplyMult;
	double mMobMeleeRageGained;
	double mMobYellowRageGained;
	double mMobMagicRageGained;
	double mRageOverflow;

	long mMobWhiteBaseDamage;
	long mMobWhiteDamageTaken;
	int mNrMobWhiteMiss;
	int mNrMobWhiteDodge;
	int mNrMobWhiteCrit;
	int mNrMobWhiteHit;

	long mMobYellowBaseDamage;
	long mMobYellowDamageTaken;
	int mNrMobYellowMiss;
	int mNrMobYellowDodge;
	int mNrMobYellowHit;

	long mMobMagicBaseDamage;
	long mMobMagicDamageTaken;
	int mNrMobMagicMiss;
	int mNrMobMagicHit;

	long mProcPhysDamage;
	long mProcSpellDamage;

	// Automatic events
	MeleeEvent mMeleeEv;
	OoCEvent mOoCEv;
	BerserkProcEvent mBerserkProcEv;
	SavageDefenseEvent mSavageDefenseEv;
	InfectedWoundsEvent mInfectedWoundsEv;
	StampedeEvent mStampedeEv;
	VengeanceEvent mVengeanceEv;
	LotPEvent mLotPEv;
	ToTTEvent mToTTEv;
	Tier_12_2pcEvent mTier_12_2pcEv;
	KirilProcEvent mKirilProcEv;

	// Player actions
	DemoralizingRoarAction mDemoralizingRoarAction;
	SkullBashAction mSkullBashAction;
	BerserkAction mBerserkAction;
	EnrageAction mEnrageAction;
	LacerateAction mLacerateAction;
	MangleAction mMangleAction;
	PulverizeAction mPulverizeAction;
	MaulAction mMaulAction;
	ThrashAction mThrashAction;
	SwipeAction mSwipeAction;
	FeralChargeAction mFeralChargeAction;
	BarkskinAction mBarkskinAction;
	FrenziedRegenerationAction mFrenziedRegenerationAction;
	SurvivalInstinctsAction mSurvivalInstinctsAction;
	BashAction mBashAction;
	PotionAction mPotionAction;

	// "Actions" that aren't really player controlled
	HeroismEvent mHeroismEv;
	UnholyFrenzyEvent mUnholyFrenzyEv;

	// Misc stuff.
	private int mNrTalentPointsNaturalReaction;
	double mFeralFFCooldownExpireTime;
	double mShatteringThrowCooldownExpireTime;

	// Stash damage taken for giggles.
	List<Double> mMobWhiteDamageTakenSamples;
	List<Double> mMobYellowDamageTakenSamples;
	List<Double> mMobMagicDamageTakenSamples;

	class BearTargetData extends SimulationTarget.TargetData {
		BearTargetData(SimulationTarget t) {
			t.super(mSimEngine);

			mFeralFFAction = new FeralFFAction(BearThreatSimulator.this, this);
			mShatteringThrowEv = new ShatteringThrowEvent(BearThreatSimulator.this, this);
		}

		FeralFFAction mFeralFFAction;
		ShatteringThrowEvent mShatteringThrowEv;
	}

	BearThreatSimulator(SimulationEngine su) {
		mSimEngine = su;
		mModel = new BearModel(mSimEngine.getToon(), mSimEngine.getConfig());
		mModel.setSimulationEngine(mSimEngine);

		mActiveTarget = mSimEngine.getPrimaryTarget();
		mActiveTarget.setTargetData(new BearTargetData(mActiveTarget));
		mActiveTargetData = (BearTargetData) mActiveTarget.getTargetData();
		mModel.setTarget(mActiveTarget.getTarget());
		mTargetList = new ArrayList<SimulationTarget>();
		mTargetList.add(mActiveTarget);

		mIsLogging = mSimEngine.isLogging();
		mLog = mSimEngine.getLog();

		mUnlimitedRage = mSimEngine.getConfig().getBool(SimulatorProperty.UNLIMITEDRAGE);
		mShouldFF = mModel.mBuffsDebuffs.isDebuff(Debuff.ARMOR);

		mMobWhiteDamageTakenSamples = mActiveTarget.getTargetSwingEv().isUp() ? new ArrayList<Double>((int) Math.ceil(mSimEngine.getEndTime() / mActiveTarget.getTarget().getSwingInterval())) : null;
		mMobYellowDamageTakenSamples = mActiveTarget.getTargetYellowEv().isUp() ? new ArrayList<Double>((int) Math.ceil(mSimEngine.getEndTime() / mActiveTarget.getTarget().getYellowInterval())) : null;
		mMobMagicDamageTakenSamples = mActiveTarget.getTargetMagicEv().isUp() ? new ArrayList<Double>((int) Math.ceil(mSimEngine.getEndTime() / mActiveTarget.getTarget().getMagicInterval())) : null;

		// Initialize rage.
		mRage = mUnlimitedRage ? 100 : 0;
		double pFuror = (mModel.mToon.getTalent(BearModel.FUROR) == 3) ? 1.0 : 0.33 * mModel.mToon.getTalent(BearModel.FUROR);
		if (mSimEngine.getRandom().nextBoolean(pFuror))
			onRageGain(10);

		// Setup events.
		mMeleeEv = new MeleeEvent(this);
		mOoCEv = new OoCEvent(this);
		mBerserkProcEv = new BerserkProcEvent(this);
		mSavageDefenseEv = new SavageDefenseEvent(this);
		mInfectedWoundsEv = new InfectedWoundsEvent(this);
		mStampedeEv = new StampedeEvent(this);
		mVengeanceEv = new VengeanceEvent(this);
		mLotPEv = new LotPEvent(this);
		mToTTEv = new ToTTEvent(this);
		mTier_12_2pcEv = new Tier_12_2pcEvent(this);
		mKirilProcEv = new KirilProcEvent(this);
		if (!mModel.mBuffsDebuffs.isDebuff(Debuff.SHATTERINGTHROW)) mShatteringThrowCooldownExpireTime = Double.MAX_VALUE;

		// Setup incoming damage handlers.
		mNrTalentPointsNaturalReaction = mModel.mToon.getTalent(BearModel.NATURALREACTION);
		mActiveTarget.getTargetSwingEv().setCharacterModel(mModel);
		mActiveTarget.getTargetSwingEv().addTargetSwingCallback(new TargetSwingCallback() {
			@Override
			public void onTargetSwing(AttackResult result, int baseDamage, int damage) {
				//
				// Calculate rage gain.
				//
				// Per EJ:
				//  UD = Unmitigated Damage Taken, a = constant, and H = Maximum Health.
				//  Gain >= 2:
				//   R = (UD*a)/H = Rage
				//   a = 20.1 w/o NR, 18.92 w NR
				//  Gain < 2:
				//   R = 1 + (UD*b)/H
				//   b = 9.77 w NR (No idea about without, but everyone should have it so who cares?)
				//
				// Notes:
				//  * FIXME: Barkskin/SI affect Rage (How?).  Armor does not.
				//
				// Source:
				//  * http://elitistjerks.com/f73/t105117-cataclysm_bear_theorycraft/#post1767812
				//

				double baseRageDamage = (double) baseDamage;
//				baseRageDamage *= mBarkskinAction.isBarkskinUp() ? mModel.multBarkskinDR : 1.0;
//				baseRageDamage *= mSurvivalInstinctsAction.isUp() ? mModel.multSurvivalInstinctsDR : 1.0;

				double a = 20.1 - 0.59 * mNrTalentPointsNaturalReaction;
				double b = 9.77;
				double rageGained = baseRageDamage * a / mModel.getHealth();
				if (rageGained < 2)
					rageGained = 1.0f + baseRageDamage * b / mModel.getHealth();

				mMobMeleeRageGained += rageGained;
				onRageGain(rageGained);
				if (result == AttackResult.MISS) {
					mVengeanceEv.onAttacked(0);
					mNrMobWhiteMiss++;

					mMobWhiteDamageTakenSamples.add(0.0);

					if (mIsLogging) mLog.log("Target White Attack: MISS\n");
				} else if (result == AttackResult.DODGE) {
					mNaturalReactionRageGained += mModel.naturalReactionRage;
					onRageGainThreat(mModel.naturalReactionRage);
					mVengeanceEv.onAttacked(0);
					mNrMobWhiteDodge++;

					mMobWhiteDamageTakenSamples.add(0.0);

					if (mIsLogging) mLog.log("Target White Attack: DODGE\n");
				} else {
					// Hit/Crit handling is mostly unified.
					int preSDDamage = damage;
					if (result == AttackResult.CRIT) mNrMobWhiteCrit++; else mNrMobWhiteHit++;
					damage = mSavageDefenseEv.onDamage(damage);
					mVengeanceEv.onAttacked(damage > 0 ? preSDDamage : 0);
					mMobWhiteDamageTaken += damage;

					mMobWhiteDamageTakenSamples.add((double) damage);

					if (mIsLogging) mLog.log("Target White Attack: %s Damage: %d Absorbed: %d\n", result, Math.round(damage), Math.round(preSDDamage - damage));
				}

				mMobWhiteBaseDamage += baseDamage;
			}
		});

		mActiveTarget.getTargetYellowEv().setCharacterModel(mModel);
		mActiveTarget.getTargetYellowEv().addTargetYellowCallback(new TargetYellowCallback() {
			@Override
			public void onTargetYellow(AttackResult result, int baseDamage, int damage) {
				//
				// Calculate rage gain.
				//
				// XXX: This is supposedly different for yellow attacks.  But since I have no way
				// of working out the formula, use the white attack one for now.
				//
				// Per EJ:
				//  UD = Unmitigated Damage Taken, a = constant, and H = Maximum Health.
				//  Gain >= 2:
				//   R = (UD*a)/HR = Rage
				//   a = 20.1 w/o NR, 18.92 w NR
				//  Gain < 2:
				//   R = 1 + (UD*b)/H
				//   b = 9.77 w NR (No idea about without, but everyone should have it so who cares?)
				//
				// Notes:
				//  * (FIXME) Barkskin/SI affect Rage (How?).  Armor does not.
				//

				double baseRageDamage = (double) baseDamage;
//				baseRageDamage *= mBarkskinAction.isBarkskinUp() ? mModel.multBarkskinDR : 1.0;
//				baseRageDamage *= mSurvivalInstinctsAction.isUp() ? mModel.multSurvivalInstinctsDR : 1.0;

				double a = 20.1 - 0.59 * mNrTalentPointsNaturalReaction;
				double b = 9.77;

				double rageGained = baseRageDamage * a / mModel.getHealth();
				if (rageGained < 2)
					rageGained = 1.0f + baseRageDamage * b / mModel.getHealth();

				mMobYellowRageGained += rageGained;
				onRageGain(rageGained);
				if (result == AttackResult.MISS) {
					mVengeanceEv.onAttacked(0);
					mNrMobYellowMiss++;

					mMobYellowDamageTakenSamples.add(0.0);

					if (mIsLogging) mLog.log("Target Yellow Attack: MISS\n");
				} else if (result == AttackResult.DODGE) {
					mNaturalReactionRageGained += mModel.naturalReactionRage;
					onRageGainThreat(mModel.naturalReactionRage);
					mVengeanceEv.onAttacked(0);
					mNrMobYellowDodge++;

					mMobYellowDamageTakenSamples.add(0.0);

					if (mIsLogging) mLog.log("Target Yellow Attack: DODGE\n");
				} else {
					int preSDDamage = damage;
					mNrMobYellowHit++;
					damage = mSavageDefenseEv.onDamage(damage);
					mVengeanceEv.onAttacked(damage > 0 ? preSDDamage : 0);
					mMobYellowDamageTaken += damage;

					mMobYellowDamageTakenSamples.add((double) damage);

					if (mIsLogging) mLog.log("Target Yellow Attack: HIT Damage: %d Absorbed: %d\n", Math.round(damage), Math.round(preSDDamage - damage));
				}

				mMobYellowBaseDamage += baseDamage;
			}
		});

		mActiveTarget.getTargetMagicEv().setCharacterModel(mModel);
		mActiveTarget.getTargetMagicEv().addTargetMagicCallback(new TargetMagicCallback() {
			@Override
			public void onTargetMagic(AttackResult result, int baseDamage, int damage) {
				//
				// Calculate rage gain.
				//
				// Per EJ:
				//  R = Rage gained, D = Damage taken, a = 40.5, and H = Maximum Health
				//  Gain >= 2:
				//   R = D*a/H
				//  Gain < 2:
				//   R = 1 + D*a1/H where a1 = 20.25
				//
				double a = 40.5;
				double b = 20.25;
				double rageGained = (double) damage * a / mModel.getHealth();
				if (rageGained < 2)
					rageGained = 1.0f + (double) damage * b / mModel.getHealth();

				mMobMagicRageGained += rageGained;
				onRageGain(rageGained);
				if (result == AttackResult.MISS) {
					mVengeanceEv.onAttacked(0);
					mNrMobMagicMiss++;

					mMobMagicDamageTakenSamples.add(0.0);

					if (mIsLogging) mLog.log("Target Magic Attack: MISS\n");
				} else {
					mNrMobMagicHit++;
					mVengeanceEv.onAttacked(damage);
					mMobMagicDamageTaken += damage;

					mMobMagicDamageTakenSamples.add((double) damage);

					if (mIsLogging) mLog.log("Target Magic Attack: HIT Damage %d\n", Math.round(damage));
				}

				mMobMagicBaseDamage += baseDamage;
			}
		});

		mSimEngine.registerProcEffectCallback(Effect.PHYSICAL_DAMAGE, new EffectCallback() {
			@Override
			public void onEffect(SimulationTarget target, int n) {
				if (n <= 0) return;

				AttackResult result = mModel.getYellowResult();
				double damage = n;

				if (result != AttackResult.MISS && result != AttackResult.DODGE) {
					if (result == AttackResult.CRIT) {
						damage *= mModel.multCrit;
					}

					damage *= getDirectDamageMult();
					mProcPhysDamage += Math.round(damage);

					if (mIsLogging) mLog.logAppend(" %s Damage: %d", result, Math.round(damage));
				} else {
					if (mIsLogging) mLog.logAppend(" %s", result);
				}
			}
		});
		mSimEngine.registerProcEffectCallback(Effect.SPELL_DAMAGE, new EffectCallback() {
			@Override
			public void onEffect(SimulationTarget target, int n) {
				if (n <= 0) return;

				AttackResult result = mModel.getSpellResult();
				double damage = n;

				if (result != AttackResult.MISS) {
					if (result == AttackResult.CRIT) {
						damage *= mModel.multSpellCrit;
					}

					damage *= getSpellDamageMult();

					mProcSpellDamage += Math.round(damage);

					if (mIsLogging) mLog.logAppend(" %s Damage: %d", result, Math.round(damage));
				} else {
					if (mIsLogging) mLog.logAppend(" MISS");
				}
			}
		});
		mSimEngine.registerProcEffectCallback(Effect.SPELL_ID, new EffectCallback() {
			int mMatrixRestabilizerNormalStat;
			int mMatrixRestabilizerHeroicStat;

			@Override
			public void onEffect(SimulationTarget target, int n) {
				// Matrix Restabilizer
				if (n == 69150 || n == 68994) {
					double critRating = mModel.getCritRating();
					double hasteRating = mModel.getHasteRating();
					double masteryRating = mModel.getMasteryRating();

					/*
					 * Cases tested by my minion:
					 * 0 of all ratings - Mastery
					 * 77 Mastery, 177 Crit and 103 Haste - Crit (No interaction with base mastery)
					 * 143 Mastery, 143 Crit and 0 Haste - Mastery
					 * 115 Mastery, 143 Crit, and 143 Haste - Haste
					 *
					 * It seems reasonable to conclude that the trinket follows Mastery > Haste > Crit
					 */
					int stateVar = 0;
					if (masteryRating >= critRating && masteryRating >= hasteRating) {
						stateVar = 2;
						mModel.addMasteryRating(Procs.getInstance().getProcByItemID(n).get(Effect.MASTERY_RATING));
						if (mIsLogging) mLog.logAppend(" Proc: Mastery Rating");
					} else if (hasteRating >= critRating && hasteRating >= masteryRating) {
						stateVar = 1;
						mModel.addHasteRating(Procs.getInstance().getProcByItemID(n).get(Effect.HASTE_RATING));
						if (mIsLogging) mLog.logAppend(" Proc: Haste Rating");
					} else if (critRating >= hasteRating && critRating >= masteryRating) {
						stateVar = 0;
						mModel.addCritRating(Procs.getInstance().getProcByItemID(n).get(Effect.CRIT_RATING));
						if (mIsLogging) mLog.logAppend(" Proc: Crit. Rating");
					}

					if (n == 69150) mMatrixRestabilizerHeroicStat = stateVar; else mMatrixRestabilizerNormalStat = stateVar;
				} else if (n == -69150 || n == -68994) {
					int stateVar = (n == -69150) ? mMatrixRestabilizerHeroicStat: mMatrixRestabilizerNormalStat;
					switch (stateVar) {
					case 0: mModel.addCritRating(-Procs.getInstance().getProcByItemID(-n).get(Effect.CRIT_RATING)); break;
					case 1: mModel.addHasteRating(-Procs.getInstance().getProcByItemID(-n).get(Effect.HASTE_RATING)); break;
					case 2: mModel.addMasteryRating(-Procs.getInstance().getProcByItemID(-n).get(Effect.MASTERY_RATING)); break;
					}
				}

				// DMC:Hurricane
				// As of 4.0.6, use the melee crit multipler/hit table.
				if (n == 62051) {
					// Just use the stealth yellow attack table resolution since it ignores Dodge/Glance.
					AttackResult result = mModel.getYellowStealthResult(0);

					double damage = Procs.getInstance().getProcByItemID(n).get(Effect.SPELL_DAMAGE);

					if (result != AttackResult.MISS) {
						// As of 4.2 PTR, this is no longer capable of critting.
//						if (result == AttackResult.CRIT) {
//							damage *= mModel.multCrit;
//						}

						damage *= getSpellDamageMult();

						if (mIsLogging) mLog.logAppend(" Damage: %d", Math.round(damage));

						mProcSpellDamage += Math.round(damage);
					} else {
						if (mIsLogging) mLog.logAppend(" MISS");
					}
				}

				// Vial of Shadows
				if (n == 77999 || n == 77207 || n == 77979) {
					AttackResult result = mModel.getFrontalYellowResult();

					if (result == AttackResult.MISS) {
						if (mIsLogging) mLog.logAppend(" MISS");
					} else if (result == AttackResult.DODGE) {
						if (mIsLogging) mLog.logAppend(" DODGE");
					} else if (result == AttackResult.PARRY) {
						if (mIsLogging) mLog.logAppend(" PARRY");
					} else {
						double damage = Procs.getInstance().getProcByItemID(n).get(Effect.PHYSICAL_DAMAGE);

						double coeff = 0.0;
						switch (n) {
						case 77999: coeff = 0.333; break;	// Heroic
						case 77207: coeff = 0.300; break;	// Normal
						case 77979: coeff = 0.266; break;	// LFR
						}

						damage += mModel.getAttackPower() * coeff;

						if (result == AttackResult.CRIT) {
							damage *= mModel.multCrit;
						}

						// XXX: Is this mitigated by armor? (Assumes yes).
						damage *= getDirectDamageMult();
						mProcPhysDamage += damage;

						if (mIsLogging) mLog.logAppend(" Damage: %d", Math.round(damage));
					}
				}

				// Kiril, Fury of the Beasts
				// Note: The event code handles cleanup on it's own since it schedules an event to do ramp up.
				if (n == 78473 || n == 77194 || n == 78482) {
					mKirilProcEv.run(n);
				}

				// Troll Berserking (Racial)
				if (n == 26297) {
					mModel.multiplyMeleeHaste(1.2);
					mModel.multiplySpellHaste(1.2);
				} else if (n == -26297) {
					mModel.multiplySpellHaste(1.0 / 1.2);
					mModel.multiplyMeleeHaste(1.0 / 1.2);
				}
			}
		});

		// Proc support.
		for (ProcEffect effect : Procs.getInstance().getProcsByProperty(mSimEngine.getConfig().getProperties())) {
			if (effect.triggers.equals(EnumSet.of(ProcEffect.Trigger.POTION))) {
				mHavePotion = true;
				mPrePotion = su.getConfig().getBool(Property.SIMULATION_PREPOTION);
			} else if (!effect.triggers.contains(ProcEffect.Trigger.USER) && effect.rate == 0 && effect.ppm == 0) {
				mSimEngine.getView().onWarn("Proc effect \"" + effect.name + "\" has no rate information.");
				continue;
			}

			mSimEngine.addProc(new SimulationProc(mSimEngine, mModel, effect));
		}

		// Wire up the actions.
		mDemoralizingRoarAction = new DemoralizingRoarAction(this);
		mSkullBashAction = new SkullBashAction(this);
		mBerserkAction = new BerserkAction(this);
		mEnrageAction = new EnrageAction(this);
		mLacerateAction = new LacerateAction(this);
		mMangleAction = new MangleAction(this);
		mPulverizeAction = new PulverizeAction(this);
		mMaulAction = new MaulAction(this);
		mThrashAction = new ThrashAction(this);
		mSwipeAction = new SwipeAction(this);
		mFeralChargeAction = new FeralChargeAction(this);
		mBarkskinAction = new BarkskinAction(this);
		mFrenziedRegenerationAction = new FrenziedRegenerationAction(this);
		mSurvivalInstinctsAction = new SurvivalInstinctsAction(this);
		mBashAction = new BashAction(this);
		mPotionAction = new PotionAction(this);
		mHeroismEv = new HeroismEvent(this);
		mUnholyFrenzyEv = new UnholyFrenzyEvent(this);

		mSimEngine.addAction(Action.DEMORALIZINGROAR, mDemoralizingRoarAction);
		mSimEngine.addAction(Action.FERALFF, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				if (mFeralFFCooldownExpireTime > mSimEngine.getSimTime())
					return ACTION_FAIL;

				mFeralFFCooldownExpireTime = mSimEngine.getSimTime() + mModel.feralFFCDDuration;

				return mActiveTargetData.mFeralFFAction.perform();
			}
		});
		mSimEngine.addAction(Action.SKULLBASH, mSkullBashAction, false);
		mSimEngine.addAction(Action.FERALCHARGE, mFeralChargeAction, false);
		mSimEngine.addAction(Action.BERSERK, mBerserkAction, false);
		mSimEngine.addAction(Action.ENRAGE, mEnrageAction, false);
		mSimEngine.addAction(Action.LACERATE, mLacerateAction);
		mSimEngine.addAction(Action.MANGLE, mMangleAction);
		mSimEngine.addAction(Action.PULVERIZE, mPulverizeAction);
		mSimEngine.addAction(Action.MAUL, mMaulAction, false);
		mSimEngine.addAction(Action.THRASH, mThrashAction);
		mSimEngine.addAction(Action.SWIPE, mSwipeAction);
		mSimEngine.addAction(Action.POTION, mPotionAction, false);
		mSimEngine.addAction(Action.HEROISM, mHeroismEv, false);
		mSimEngine.addAction(Action.UNHOLYFRENZY, mUnholyFrenzyEv, false);
		mSimEngine.addAction(Action.SHATTERINGTHROW, new SimulationAction() {
			@Override
			public double perform() {
				if (mActiveTargetData == null) return ACTION_FAIL;

				if (mShatteringThrowCooldownExpireTime > mSimEngine.getSimTime())
					return ACTION_FAIL;

				mShatteringThrowCooldownExpireTime = mSimEngine.getSimTime() + mModel.shatteringThrowCDDuration;

				return mActiveTargetData.mShatteringThrowEv.perform();
			}
		}, false);

		mSimEngine.addAction(Action.BARKSKIN, mBarkskinAction, false);
		mSimEngine.addAction(Action.FRENZIEDREGENERATION, mFrenziedRegenerationAction, false);
		mSimEngine.addAction(Action.SURVIVALINSTINCTS, mSurvivalInstinctsAction, false);

		mSimEngine.addAction(Action.BASH, mBashAction);

		mSimEngine.addAction(Action.AUTOATTACK_STOP, new SimulationAction() {
			@Override
			public double perform() {
				mMeleeEv.stop();
				return 0.0;
			}
		}, false);
		mSimEngine.addAction(Action.AUTOATTACK_START, new SimulationAction() {
			@Override
			public double perform() {
				mMeleeEv.start();
				return mMeleeEv.isUp() ? 0.0 : ACTION_FAIL;
			}
		}, false);
		mSimEngine.addAction(Action.NONE, new SimulationAction() {
			@Override
			public double perform() {
				return ACTION_FAIL;
			}
		});
	}

	final boolean hasRage(int rage) {
		if (mUnlimitedRage) return true;

		return mRage >= rage;
	}

	final double onRageGain(double rage) {
		double before = mRage;

		mRage = mRage + rage;
		if (mRage > 100) {
			mRageOverflow += (mRage - 100);
			mRage = 100;
		}

		return (mUnlimitedRage) ? rage : mRage - before;
	}

	final void onRageGainThreat(double rage) {
		mRageGainedCountThreat += Math.floor(onRageGain(rage));
	}

	final void onRageGainThreatApplyMult(double rage) {
		mRageGainedCountThreatApplyMult += Math.floor(onRageGain(rage));
	}

	final void onRageLoss(int rage) {
		onRageLoss(rage, true);
	}

	final void onRageLoss(int rage, boolean consumeOoC) {
		if (consumeOoC && mOoCEv.isUp()) {
			mOoCEv.mOoCRageReturn += rage;
			mOoCEv.cancel();
		} else {
			if (!mUnlimitedRage) mRage = Math.max(0, mRage - rage);

			mRageSpent += rage;
		}
	}

	final void onPrimalFuryTrigger() {
		if (mSimEngine.getRandom().nextBoolean(mModel.pPrimalFury)) {
			onRageGainThreat(5);
			mPrimalFuryRageGained += 5;
		}
	}

	final double getDirectDamageMult() {
		double mult = 1;

		mult *= (1 - mActiveTargetData.getArmorDR(mModel.mToon.level));
		mult *= mModel.multPhysVuln;
		mult *= mModel.multDamageMult;
		mult *= mModel.multMasterShapeshifter;
		mult *= mToTTEv.getMult();
		mult *= mEnrageAction.getMult();

		return mult;
	}

	final double getBleedDamageMult() {
		double mult = 1;

		// ToTT and Enrage are handled elsewhere.
		mult *= mModel.multBleedDarkIntent;
		mult *= mMangleAction.isUp() ? mModel.multMangle : 1;
		mult *= mModel.multPhysVuln * mModel.multDamageMult;
		mult *= mModel.multMasterShapeshifter;

		return mult;
	}

	final double getSpellDamageMult() {
		double mult = 1;

		mult *= mModel.multSpellVuln;
		mult *= mModel.multDamageMult;
		mult *= mToTTEv.getMult();
		mult *= mEnrageAction.getMult();

		return mult;
	}

	final void onPrimalMadnessGain() {
		mPrimalMadnessRageGained += mModel.primalMadnessRage;
		onRageGainThreat(mModel.primalMadnessRage);
	}

	final void onPrimalMadnessLoss() {
		// I assume that the rage doesn't go away.
	}

	// Begin Public Methods available to Strategy
	public final double getEncounterDuration() {
		return mSimEngine.getEndTime();
	}

	public final double getTimeRemaining() {
		return mSimEngine.getEndTime() - mSimEngine.getSimTime();
	}

	public final double getElapsedTime() {
		return mSimEngine.getSimTime();
	}

	public final int getLevel() {
		return mSimEngine.getToon().level;
	}

	public final boolean isInCombat() {
		return mSimEngine.getSimTime() > 0.0;
	}

	public final boolean isInGCD() {
		return mSimEngine.isInGCD();
	}

	public final boolean isAutoAttackEnabled() {
		return mMeleeEv.isUp();
	}

	public final int getRage() {
		return (int) Math.floor(mRage);
	}

	public final boolean hasUnlimitedRage() {
		return mUnlimitedRage;
	}

	public final boolean isOoCUp() {
		return mOoCEv.isUp();
	}

	public final boolean hasPotion() {
		return mHavePotion;
	}

	public final boolean canPotion() {
		// Don't potion out of combat if the user does not want pre-potion.
		if (!mPrePotion && !isInCombat()) return false;

		return mPotionAction.canPotion();
	}

	public final boolean shouldSkullBash() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.getSimulationTarget().getTargetCastEv().isUp();
	}

	public final boolean isTargetCasting() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.getSimulationTarget().getTargetCastEv().isCasting();
	}

	public final double nextCastIn() {
		if (mActiveTargetData == null) return 0.0;
		return mActiveTargetData.getSimulationTarget().getTargetCastEv().nextCastIn();
	}

	public final double getTargetCastTime() {
		if (mActiveTargetData == null) return 0.0;
		return mActiveTargetData.getSimulationTarget().getTargetCastEv().getTargetCastTime();
	}

	// Ability related queries here.
	public final boolean isDemoralizingRoarUp() {
		return mDemoralizingRoarAction.isUp();
	}

	public final double getDemoralizingRoarRemaining() {
		return mDemoralizingRoarAction.isUp() ? mDemoralizingRoarAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isInfectedWoundsUp() {
		return mInfectedWoundsEv.isUp();
	}

	public final double getInfectedWoundsRemaining() {
		return mInfectedWoundsEv.isUp() ? mInfectedWoundsEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean shouldFF() {
		return mShouldFF;
	}

	public final boolean isFeralFFUp() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.mFeralFFAction.mStacks > 0;
	}

	public final int getFeralFFStacks() {
		if (mActiveTargetData == null) return 0;
		return mActiveTargetData.mFeralFFAction.mStacks;
	}

	public final double getFeralFFRemaining() {
		return isFeralFFUp() ? mActiveTargetData.mFeralFFAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getFeralFFCD() {
		return mFeralFFCooldownExpireTime > mSimEngine.getSimTime() ? mFeralFFCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getFeralChargeCD() {
		return mFeralChargeAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mFeralChargeAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean canFeralCharge() {
		if (isInCombat()) return false;

		// Only allow this if we have points in Stampede.
		return (getFeralChargeCD() == 0.0 && mRage > getFeralChargeCost() && mModel.mToon.getTalent(BearModel.STAMPEDE) > 0);
	}

	public final int getFeralChargeCost() {
		return mModel.feralChargeCost;
	}

	public final double getSkullBashCD() {
		return mSkullBashAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mSkullBashAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final int getSkullBashCost() {
		return mModel.skullBashCost;
	}

	public final boolean isMangleUp() {
		return mMangleAction.isUp();
	}

	public final double getMangleRemaining() {
		return mMangleAction.isUp() ? mMangleAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getMangleCD() {
		return mMangleAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mMangleAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final int getMangleCost() {
		if (isBerserkUp() || isBerserkProcUp() || isOoCUp()) return 0;

		return mModel.mangleCost;
	}

	public final boolean isBerserkUp() {
		return mBerserkAction.isUp();
	}

	public final double getBerserkCD() {
		return mBerserkAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mBerserkAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getBerserkRemaining() {
		return mBerserkAction.isUp() ? mBerserkAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getBerserkBaseDuration() {
		return mModel.berserkBuffDuration;
	}

	public final boolean isBerserkProcUp() {
		return mBerserkProcEv.isUp();
	}

	public final double getBerserkProcRemaining() {
		return mBerserkProcEv.isUp() ? mBerserkProcEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isEnrageUp() {
		return mEnrageAction.isUp();
	}

	public final double getEnrageCD() {
		return mEnrageAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mEnrageAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getEnrageRemaining() {
		return mEnrageAction.isUp() ? mEnrageAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getMaulCD() {
		return mMaulAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mMaulAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final int getMaulCost() {
		return isOoCUp() ? 0 : mModel.maulCost;
	}

	public final double getSwipeCD() {
		return mSwipeAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mSwipeAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final int getSwipeCost() {
		return isOoCUp() ? 0 : mModel.swipeCost;
	}

	public final boolean isThrashUp() {
		return mThrashAction.isUp();
	}

	public final double getThrashRemaining() {
		return mThrashAction.isUp() ? mThrashAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final double getThrashCD() {
		return mThrashAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mThrashAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final int getThrashCost() {
		return isOoCUp() ? 0 : mModel.thrashCost;
	}

	public final boolean isLacerateUp() {
		return mLacerateAction.isUp();
	}

	public final double getLacerateRemaining() {
		return mLacerateAction.isUp() ? mLacerateAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final int getLacerateStacks() {
		return mLacerateAction.getStacks();
	}

	public final int getLacerateCost() {
		return isOoCUp() ? 0 : mModel.lacerateCost;
	}

	public final boolean isPulverizeUp() {
		return mPulverizeAction.isUp();
	}

	public final double getPulverizeRemaining() {
		return mPulverizeAction.isUp() ? mPulverizeAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final int getPulverizeLacStacks() {
		return mPulverizeAction.getStacks();
	}

	public final int getPulverizeCost() {
		return isOoCUp() ? 0 : mModel.pulverizeCost;
	}

	public final boolean canPulverize() {
		return mPulverizeAction.canPulverize() && (hasRage(getPulverizeCost()));
	}

	public final boolean hasPulverize() {
		return mPulverizeAction.hasPulverize();
	}

	public final int getPulverizeBaseDuration() {
		return mPulverizeAction.canPulverize() ? mModel.basePulverizeBuffDuration + mModel.lacPulverizeBuffDuration : 0;
	}

	public final boolean isBarkskinUp() {
		return mBarkskinAction.isBarkskinUp();
	}

	public final double getBarkskinCD() {
		return mBarkskinAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mBarkskinAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getBarkskinRemaining() {
		return mBarkskinAction.isUp() ? mBarkskinAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isFrenziedRegenerationUp() {
		return mFrenziedRegenerationAction.isUp();
	}

	public final double getFrenziedRegenerationCD() {
		return mFrenziedRegenerationAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mFrenziedRegenerationAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getFrenziedRegenerationRemaining() {
		return mFrenziedRegenerationAction.isUp() ? mFrenziedRegenerationAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isSurvivalInstinctsUp() {
		return mSurvivalInstinctsAction.isUp();
	}

	public final double getSurvivalInstinctsCD() {
		return mSurvivalInstinctsAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mSurvivalInstinctsAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getSurvivalInstinctsRemaining() {
		return mSurvivalInstinctsAction.isUp() ? mSurvivalInstinctsAction.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final int getBashCost() {
		return mOoCEv.isUp() ? 0 : mModel.bashCost;
	}

	public final double getBashCD() {
		return mBashAction.mCooldownExpireTime > mSimEngine.getSimTime() ? mBashAction.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isHeroismUp() {
		return mHeroismEv.isUp();
	}

	public final double getHeroismCD() {
		return mHeroismEv.mCooldownExpireTime > mSimEngine.getSimTime() ? mHeroismEv.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getHeroismRemaining() {
		return mHeroismEv.isUp() ? mHeroismEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isUnholyFrenzyUp() {
		return mUnholyFrenzyEv.isUp();
	}

	public final double getUnholyFrenzyCD() {
		return mUnholyFrenzyEv.mCooldownExpireTime > mSimEngine.getSimTime() ? mUnholyFrenzyEv.mCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getUnholyFrenzyRemaining() {
		return mUnholyFrenzyEv.isUp() ? mUnholyFrenzyEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final boolean isShatteringThrowUp() {
		if (mActiveTargetData == null) return false;
		return mActiveTargetData.mShatteringThrowEv.isUp();
	}

	public final double getShatteringThrowCD() {
		return mShatteringThrowCooldownExpireTime > mSimEngine.getSimTime() ? mShatteringThrowCooldownExpireTime - mSimEngine.getSimTime() : 0.0;
	}

	public final double getShatteringThrowRemaining() {
		return isShatteringThrowUp() ? mActiveTargetData.mShatteringThrowEv.getExpiryTime() - mSimEngine.getSimTime() : 0.0;
	}

	public final Map<String, SimulationProcStatus> getProcStatusMap() {
		return mSimEngine.getProcStatusMap();
	}

	public final boolean hasTier_13_2pc() {
		return mModel.mToon.tier_13_2pc;
	}

	// Script visible functions for managing incoming damage.
	// Some of these are not strictly status queries.

	public final boolean isTargetSwingUp() {
		return mActiveTarget.getTargetSwingEv().isUp();
	}

	public final void setTargetSwingEnable(boolean enable) {
		mActiveTarget.getTargetSwingEv().setEnable(enable);
	}

	public final int getTargetSwingDamage() {
		return mActiveTarget.getTargetSwingEv().getBaseDamage();
	}

	public final double getTargetSwingInterval() {
		return mActiveTarget.getTargetSwingEv().getInterval();
	}

	public final void setTargetSwingDamage(int damage) {
		if (damage >= 0) mActiveTarget.getTargetSwingEv().setBaseDamage(damage);
	}

	public final void setTargetSwingInterval(double interval) {
		if (interval >= 0) mActiveTarget.getTargetSwingEv().setInterval(interval);
	}

	public final boolean isTargetYellowUp() {
		return mActiveTarget.getTargetYellowEv().isUp();
	}

	public final void setTargetYellowEnable(boolean enable) {
		mActiveTarget.getTargetYellowEv().setEnable(enable);
	}

	public final int getTargetYellowDamage() {
		return mActiveTarget.getTargetYellowEv().getBaseDamage();
	}

	public final double getTargetYellowInterval() {
		return mActiveTarget.getTargetYellowEv().getInterval();
	}

	public final void setTargetYellowDamage(int damage) {
		if (damage >= 0) mActiveTarget.getTargetYellowEv().setBaseDamage(damage);
	}

	public final void setTargetYellowInterval(double interval) {
		if (interval >= 0) mActiveTarget.getTargetYellowEv().setInterval(interval);
	}

	public final boolean isTargetMagicUp() {
		return mActiveTarget.getTargetMagicEv().isUp();
	}

	public final void setTargetMagicEnable(boolean enable) {
		mActiveTarget.getTargetMagicEv().setEnable(enable);
	}

	public final int getTargetMagicDamage() {
		return mActiveTarget.getTargetMagicEv().getBaseDamage();
	}

	public final double getTargetMagicInterval() {
		return mActiveTarget.getTargetMagicEv().getInterval();
	}

	public final void setTargetMagicDamage(int damage) {
		if (damage >= 0) mActiveTarget.getTargetMagicEv().setBaseDamage(damage);
	}

	public final void setTargetMagicInterval(double interval) {
		if (interval >= 0) mActiveTarget.getTargetMagicEv().setInterval(interval);
	}
}
